package com.android.segito;

import java.util.Calendar;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import android.util.Log;

/**
 * So basically this guy needs to have the algorithm implemented and then we
 * need to think how he reacts to each different different kind of call, and to
 * each life cycle event.
 */
public class Scheduler
{

	private static final String TAG = "Scheduler";

	private static PACalendar _PACalendar;
	private static NotificationManager _NotificationManager;

	private static Task _oldTask;

	/* IMPLEMENTATION */

	/**
	 * Initializer.
	 * 
	 * @param nm
	 *            notification manager.
	 * @param pacal
	 *            the calendar.
	 */
	public static void setServices(NotificationManager nm, PACalendar pacal)
	{
		_PACalendar = pacal;
		_NotificationManager = nm;
	}

	/**
	 * Schedules a new task.
	 * 
	 * @param newTask
	 *            the task to schedule.
	 * @return a list of updated tasks (for debug).
	 */
	public static List<Task> schedule(Task newTask)
	{
		Log.d(TAG, "entered schedule");
		if (newTask == null)
		{
			Log.d(TAG, "Cannot schedule null.");
			return null;
		}

		LinkedList<Task> changed = new LinkedList<Task>();

		// There could be several cases:
		// 1. Task is fixed but its time is taken by a fixed task --> schedule.
		// 2. Task is fixed but its time is taken by a fluid task --> swap and
		// reschedule fluid task, if possible.
		// 3. Task is fluid --> schedule fluid task, if possible.

		// Handling 1 and 2 (fixed task).
		if (!newTask.isFluid())
		{
			boolean isFree = _PACalendar.isFreeTime(newTask.getStartTime(),
					newTask.getEndTime());
			// If is free, add event to list, mark only it as changed and
			// return.
			if (isFree)
			{
				changed.add(newTask);
				commit(changed);
				return changed;
			}

			// Find overlapping task.
			LinkedList<Task> overlapping = _PACalendar.getSchedule(
					newTask.getStartTime(), newTask.getEndTime(), true);

			assert (overlapping != null && overlapping.size() > 0);

			Task overlappingTask = overlapping.get(0);

			// Case 1 - fixed on fixed --> schedule anyway.
			if (!overlappingTask.isFluid())
			{
				changed.add(newTask);
				commit(changed);
				return changed;
			}

			// Case 2. We check if the fluid task can be handled.
			// WE DO NOT HANDLE CASES WHERE MORE THAN ONE TASK IS OVERLAPPING.
			Calendar when = findSlot((FluidTask) overlappingTask, null);

			if (when == null)
				return null; // No slot found.

			// Found slot, schedule fixed task in place of fluid and reschedule
			// fluid task.
			changed.add(newTask);

			_oldTask = overlappingTask.clone();
			addStartEndToFluid((FluidTask) overlappingTask, when);
			changed.add(overlappingTask);

			commit(changed);
			return changed;
		} // Handles fixed task.

		// Case 3 - schedule fluid task.
		FluidTask newnewTask = (FluidTask) newTask;

		// First check if we have time now.
		Calendar s = Calendar.getInstance();
		s.roll(Calendar.MINUTE, 5);
		Calendar e = Calendar.getInstance();
		e.setTimeInMillis(s.getTimeInMillis() + newnewTask.getDuration());
		if (_PACalendar.isFreeTime(s, e))
		{
			addStartEndToFluid(newnewTask, s);
			changed.add(newnewTask);
			commit(changed);
			return changed;
		}

		// Case 3. Find a slot for fluid task.
		Calendar when = findSlot(newnewTask, null);

		if (when == null)
		{
			// No slot found, try to move other fluid task.
			changed = secondStage(newnewTask);
			commit(changed);
			return changed;
		}

		// Found slot for fluid task.
		addStartEndToFluid(newnewTask, when);
		changed.add(newnewTask);
		commit(changed);
		return changed;
	}

	/**
	 * Extend task.
	 * @param task the task to extend.
	 * @param timeToAdd the time to add.
	 */
	public static String addTimeToTask(FluidTask task, Long timeToAdd)
	{
		Log.d(TAG, "Add time to task");

		// Check that we do not pass deadline.
		if (task.getEndTime().getTimeInMillis() + timeToAdd >= task
				.getDeadline().getTimeInMillis())
			return "Cannot add time past deadline.";

		LinkedList<Task> changed = new LinkedList<Task>();

		// Roll for checking free time.
		Calendar intervalStart = Calendar.getInstance();
		intervalStart
				.setTimeInMillis(task.getEndTime().getTimeInMillis() + 100);

		Calendar intervalEnd = Calendar.getInstance();
		intervalEnd.setTimeInMillis(task.getEndTime().getTimeInMillis()
				+ timeToAdd);

		// If there is time to extend the task.
		if (_PACalendar.isFreeTime(intervalStart, intervalEnd))
		{
			task.getEndTime().setTimeInMillis(
					task.getEndTime().getTimeInMillis() + timeToAdd);
			changed.add(task);
			commit(changed);
			return null;
		}

		// Time is not free.
		Task next = _PACalendar.getNextTask(task, true);

		assert (next != null);

		if (!next.isFluid())
		{
			Log.d(TAG,
					"can't add more time because of next task: "
							+ next.toString());
			return "Next task is fixed and cannot be moved";
		}

		Calendar from = Calendar.getInstance();
		from.setTimeInMillis(task.getEndTime().getTimeInMillis() + timeToAdd
				+ 100);

		Calendar when = findSlot((FluidTask) next, from);

		if (when == null)
			return "Failed rescheduling.";

		// Reschedule.
		addStartEndToFluid((FluidTask) next, when);
		changed.add(next);
		task.getEndTime().setTimeInMillis(
				task.getEndTime().getTimeInMillis() + timeToAdd);
		changed.add(task);

		commit(changed);
		return null;
	}

	/**
	 * Write changes to calendar and notification manager.
	 * 
	 * @param tasks
	 *            the tasks to commit.
	 */
	private static void commit(LinkedList<Task> tasks)
	{
		if (tasks == null || tasks.isEmpty())
		{
			Log.d(TAG, "commit: null or empty task list.");
			return;
		}

		_PACalendar.updateSchedule(tasks);
		_NotificationManager.setNotification(tasks.get(0));
		
		if (tasks.size() > 1 && _oldTask != null)
			_NotificationManager.updateTask(_oldTask, tasks.get(1));

		for (Task t : tasks)
			Log.d(TAG, "Adding task:\n" + t.toString());
	}

	/**
	 * We need to postpone this task by 15 minutes.
	 * 
	 * @param task
	 *            the task to postpone.
	 * @return String representation of result.
	 */
	public static String snooze(FluidTask task)
	{
		Log.d(TAG, "snooze");

		assert (task != null);

		LinkedList<Task> changed = new LinkedList<Task>();

		// Roll for checking free time.
		Calendar intervalStart = Calendar.getInstance();
		Calendar intervalEnd = Calendar.getInstance();

		intervalStart.setTimeInMillis(task.getEndTime().getTimeInMillis() + 100);
		intervalEnd.setTimeInMillis(task.getEndTime().getTimeInMillis() + 60000 * 15);

		// Roll start and end.
		task.getStartTime().roll(Calendar.MINUTE, 15);
		task.getEndTime().roll(Calendar.MINUTE, 15);

		// If there is time to postpone the task.
		if (_PACalendar.isFreeTime(intervalStart, intervalEnd))
		{
			Log.d(TAG, "in snooze: is free time true");
			changed.add(task);
			commit(changed);
			return null;
		}

		// Time is not free.
		Log.d(TAG, "in snooze: is free time false");
		Task next = _PACalendar.getNextTask(task, true);
		Log.d(TAG, "in snooze: next task is:" + next.toString());

		assert (next != null);

		if (!next.isFluid())
			return "Your next task is fixed and cannot be moved.";

		// Other is fluid, try to reschedule it.
		Calendar when = findSlot((FluidTask) next, intervalEnd);
		if (when == null)
			return "No time found for other fluid task.";

		// Found new time, need to reschedule both.
		changed.add(task);

		addStartEndToFluid((FluidTask) next, when);
		changed.add(next);
		commit(changed);
		return null;
	}

	/**
	 * Add a start and end time to a fluid task.
	 * @param task the task to add to.
	 * @param start the start time.
	 */
	private static void addStartEndToFluid(FluidTask task, Calendar start)
	{
		task.setStart(start);
		Calendar end = Calendar.getInstance();
		end.setTimeInMillis(start.getTimeInMillis() + task.getDuration());
		task.setEnd(end);
	}

	/**
	 * Get the space before and after the task.
	 * 
	 * @param before
	 *            the previous task.
	 * @param task
	 *            the task.
	 * @param after
	 *            the following task.
	 * @return the time around the task, inclusive.
	 */
	private static long getSpaceAroundTask(Task before, Task task, Task after)
	{
		// Actual task duration.
		long duration = task.getEndTime().getTimeInMillis()
				- task.getStartTime().getTimeInMillis();

		// Before space.
		if (before != null)
			duration += task.getStartTime().getTimeInMillis()
					- before.getEndTime().getTimeInMillis();

		// After space.
		if (after != null)
			duration += after.getStartTime().getTimeInMillis()
					- task.getEndTime().getTimeInMillis();

		return duration;
	}

	/**
	 * Add start and end padding to a task list.
	 * 
	 * @param task
	 *            the task to pad.
	 * @param tasks
	 *            the task list containing the task.
	 */
	private static void addPadding(FluidTask task, List<Task> tasks)
	{
		Log.d(TAG, "adding padding");
		// If there is no task right now, add a virtual task from now of length
		// 5 minutes.
		if (!(tasks.get(0).getStartTime().getTimeInMillis() <= System
				.currentTimeMillis() && tasks.get(0).getEndTime()
				.getTimeInMillis() >= System.currentTimeMillis()))
		{
			Calendar in5Minutes = Calendar.getInstance();
			in5Minutes.roll(Calendar.MINUTE, 5);
			tasks.add(0, new FixedTask("dummyFindSlot", Calendar.getInstance(),
					in5Minutes));
		}

		// If there is no task at the deadline of the task to be scheduled, add
		// a virtual task at the end.
		if (!(tasks.get(tasks.size() - 1).getStartTime().getTimeInMillis() <= task
				.getDeadline().getTimeInMillis() && tasks.get(tasks.size() - 1)
				.getEndTime().getTimeInMillis() >= task.getDeadline()
				.getTimeInMillis()))
		{
			Calendar last5Minutes = Calendar.getInstance();
			last5Minutes
					.setTimeInMillis(task.getDeadline().getTimeInMillis() - 60000);
			tasks.add(tasks.size(), new FixedTask("dummyFindSlot2",
					last5Minutes, task.getDeadline()));
		}
	}

	/**
	 * Tries to move fluid tasks which are scheduled until this fluid task's
	 * duration.
	 * 
	 * @param task
	 *            the task for second stage.
	 * @return list of changed tasks, or null if failed.
	 */
	private static LinkedList<Task> secondStage(FluidTask task)
	{
		Log.d(TAG, "entered second stage");

		LinkedList<Task> tasks = _PACalendar.getSchedule(
				Calendar.getInstance(), task.getDeadline(), true);
		LinkedList<Task> changed = new LinkedList<Task>();

		addPadding(task, tasks);

		for (Task current2 : tasks)
		{
			if (!current2.isFluid())
				continue;

			FluidTask current = (FluidTask) current2;

			int index = tasks.indexOf(current);
			Task before = null;
			if (index >= 1)
				before = tasks.get(index - 1);
			Task after = null;
			if (index < tasks.size())
				after = tasks.get(index + 1);

			// Check if moving the current task would help.
			if (getSpaceAroundTask(before, current, after) < task.getDuration())
				continue;

			Calendar startAt = null;
			if (before != null && before.getEndTime() != null)
			{
				startAt = before.getEndTime();
				startAt.setTimeInMillis(startAt.getTimeInMillis()
						+ task.getDuration());
			}

			Calendar when = findSlot(current, startAt);

			if (when == null) // No time found.
				continue;

			// Found new time for current. Need to reschedule current and
			// schedule task.
			addStartEndToFluid(task, before.getEndTime());
			changed.add(task);

			addStartEndToFluid(current, when);
			changed.add(current);

			return changed;
		}

		return null;
	}

	/**
	 * Finds a slot for event from the start time of the first event in the list
	 * to the end time of the last event in the list. If a time is found, it
	 * updates the list of events with the schedules event.
	 * 
	 * @param event
	 *            the event to schedule.
	 * @return the time to schedule the task, or null if not found.
	 */
	private static Calendar findSlot(FluidTask task, Calendar from)
	{
		Log.d(TAG, "findSlot");
		if (from == null)
			from = Calendar.getInstance();

		// Get task list from NOW until fluid task's deadline.
		Log.d(TAG, "getting tasks from PACal");
		List<Task> tasks = _PACalendar.getSchedule(from, task.getDeadline(),
				true);

		// No obstructions --> just schedule.
		if (tasks == null || tasks.isEmpty())
		{
			from.roll(Calendar.MINUTE, 1);
			return from;
		}

		Log.d(TAG, "got " + tasks.size() + " tasks");

		addPadding(task, tasks);

		Task first = null, second = null;
		long slotSize = 0L;
		Iterator<Task> it = tasks.iterator();

		first = it.next();

		Log.d(TAG, "iterating over tasks");
		// Iterate over tasks and search for a hole.
		while (it.hasNext())
		{
			second = it.next();
			slotSize = second.getStartTime().getTimeInMillis()
					- first.getEndTime().getTimeInMillis();
			if (slotSize >= task.getDuration())
				return first.getEndTime();

			first = second;
		}

		return null;
	}

}